msg_tool\scripts\bgi\image/
img.rs

1//! Buriko General Interpreter/Ethornell Uncompressed Image File
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::img::*;
6use anyhow::Result;
7
8fn try_parse(buf: &[u8]) -> Result<u8> {
9    let mut reader = MemReaderRef::new(buf);
10    let width = reader.read_u16()?;
11    let height = reader.read_u16()?;
12    let bpp = reader.read_u16()?;
13    let _flag = reader.read_u16()?;
14    let padding = reader.read_u64()?;
15    if padding != 0 {
16        return Err(anyhow::anyhow!("Invalid padding: {}", padding));
17    }
18    if width == 0 || height == 0 {
19        return Err(anyhow::anyhow!("Invalid dimensions: {}x{}", width, height));
20    }
21    if width > 4096 || height > 4096 {
22        return Err(anyhow::anyhow!(
23            "Dimensions too large: {}x{}",
24            width,
25            height
26        ));
27    }
28    if bpp != 8 && bpp != 24 && bpp != 32 {
29        return Err(anyhow::anyhow!("Unsupported BPP: {}", bpp));
30    }
31    Ok(1)
32}
33
34#[derive(Debug)]
35/// Builder for BGI Uncompressed Image scripts.
36pub struct BgiImageBuilder {}
37
38impl BgiImageBuilder {
39    /// Creates a new instance of `BgiImageBuilder`.
40    pub const fn new() -> Self {
41        BgiImageBuilder {}
42    }
43}
44
45impl ScriptBuilder for BgiImageBuilder {
46    fn default_encoding(&self) -> Encoding {
47        Encoding::Cp932
48    }
49
50    fn build_script(
51        &self,
52        data: Vec<u8>,
53        _filename: &str,
54        _encoding: Encoding,
55        _archive_encoding: Encoding,
56        config: &ExtraConfig,
57        _archive: Option<&Box<dyn Script>>,
58    ) -> Result<Box<dyn Script>> {
59        Ok(Box::new(BgiImage::new(data, config)?))
60    }
61
62    fn extensions(&self) -> &'static [&'static str] {
63        &[]
64    }
65
66    fn script_type(&self) -> &'static ScriptType {
67        &ScriptType::BGIImg
68    }
69
70    fn is_image(&self) -> bool {
71        true
72    }
73
74    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
75        if buf_len >= 0x10 {
76            return try_parse(&buf[0..0x10]).ok();
77        }
78        None
79    }
80
81    fn can_create_image_file(&self) -> bool {
82        true
83    }
84
85    fn create_image_file<'a>(
86        &'a self,
87        data: ImageData,
88        _filename: &str,
89        writer: Box<dyn WriteSeek + 'a>,
90        options: &ExtraConfig,
91    ) -> Result<()> {
92        create_image(data, writer, options.bgi_img_scramble.unwrap_or(false))
93    }
94}
95
96#[derive(Debug)]
97/// BGI Uncompressed Image script.
98pub struct BgiImage {
99    data: MemReader,
100    width: u32,
101    height: u32,
102    color_type: ImageColorType,
103    is_scrambled: bool,
104    opt_is_scrambled: Option<bool>,
105}
106
107fn create_image<'a>(
108    mut data: ImageData,
109    mut writer: Box<dyn WriteSeek + 'a>,
110    scrambled: bool,
111) -> Result<()> {
112    writer.write_u16(data.width as u16)?;
113    writer.write_u16(data.height as u16)?;
114    if data.depth != 8 {
115        return Err(anyhow::anyhow!("Unsupported image depth: {}", data.depth));
116    }
117    match data.color_type {
118        ImageColorType::Bgr => {}
119        ImageColorType::Bgra => {}
120        ImageColorType::Grayscale => {}
121        ImageColorType::Rgb => {
122            convert_rgb_to_bgr(&mut data)?;
123        }
124        ImageColorType::Rgba => {
125            convert_rgba_to_bgra(&mut data)?;
126        }
127    }
128    let bpp = data.color_type.bpp(8);
129    writer.write_u16(bpp)?;
130    let flag = if scrambled { 1 } else { 0 };
131    writer.write_u16(flag)?;
132    writer.write_u64(0)?; // Padding
133    let stride = data.width as usize * ((data.color_type.bpp(8) as usize + 7) / 8);
134    let buf_size = stride * data.height as usize;
135    if scrambled {
136        let bpp = data.color_type.bpp(1) as usize;
137        for i in 0..bpp {
138            let mut dst = i;
139            let mut incr = 0u8;
140            let mut h = data.height;
141            while h > 0 {
142                for _ in 0..data.width {
143                    writer.write_u8(data.data[dst].wrapping_sub(incr))?;
144                    incr = data.data[dst];
145                    dst += bpp;
146                }
147                h -= 1;
148                if h == 0 {
149                    break;
150                }
151                dst += stride;
152                let mut pos = dst;
153                for _ in 0..data.width {
154                    pos -= bpp;
155                    writer.write_u8(data.data[pos].wrapping_sub(incr))?;
156                    incr = data.data[pos];
157                }
158                h -= 1;
159            }
160        }
161    } else {
162        // PNG sometimes return more padding data than expected
163        // We will write only the required size
164        writer.write_all(&data.data[..buf_size])?;
165    }
166    Ok(())
167}
168
169impl BgiImage {
170    /// Creates a new instance of `BgiImage` from a buffer.
171    ///
172    /// * `buf` - The buffer containing the script data.
173    /// * `config` - Extra configuration options.
174    pub fn new(buf: Vec<u8>, config: &ExtraConfig) -> Result<Self> {
175        let mut reader = MemReader::new(buf);
176        let width = reader.read_u16()? as u32;
177        let height = reader.read_u16()? as u32;
178        let bpp = reader.read_u16()?;
179        let color_type = match bpp {
180            8 => ImageColorType::Grayscale,
181            24 => ImageColorType::Bgr,
182            32 => ImageColorType::Bgra,
183            _ => return Err(anyhow::anyhow!("Unsupported BPP: {}", bpp)),
184        };
185        let flag = reader.read_u16()?;
186        let padding = reader.read_u64()?;
187        if padding != 0 {
188            return Err(anyhow::anyhow!("Invalid padding: {}", padding));
189        }
190        let is_scrambled = flag != 0;
191
192        Ok(BgiImage {
193            data: reader,
194            width,
195            height,
196            color_type,
197            is_scrambled,
198            opt_is_scrambled: config.bgi_img_scramble,
199        })
200    }
201}
202
203impl Script for BgiImage {
204    fn default_output_script_type(&self) -> OutputScriptType {
205        OutputScriptType::Json
206    }
207
208    fn default_format_type(&self) -> FormatOptions {
209        FormatOptions::None
210    }
211
212    fn is_image(&self) -> bool {
213        true
214    }
215
216    fn export_image(&self) -> Result<ImageData> {
217        let stride = self.width as usize * ((self.color_type.bpp(8) as usize + 7) / 8);
218        let buf_size = stride * self.height as usize;
219        let mut data = Vec::with_capacity(buf_size);
220        data.resize(buf_size, 0);
221        if self.is_scrambled {
222            let mut reader = self.data.to_ref();
223            reader.pos = 0x10;
224            let bpp = self.color_type.bpp(1) as usize;
225            for i in 0..bpp {
226                let mut dst = i;
227                let mut incr = 0u8;
228                let mut h = self.height;
229                while h > 0 {
230                    for _ in 0..self.width {
231                        incr = incr.wrapping_add(reader.read_u8()?);
232                        data[dst] = incr;
233                        dst += bpp;
234                    }
235                    h -= 1;
236                    if h == 0 {
237                        break;
238                    }
239                    dst += stride;
240                    let mut pos = dst;
241                    for _ in 0..self.width {
242                        pos -= bpp;
243                        incr = incr.wrapping_add(reader.read_u8()?);
244                        data[pos] = incr;
245                    }
246                    h -= 1;
247                }
248            }
249        } else {
250            self.data.cpeek_exact_at(0x10, &mut data)?;
251        }
252        Ok(ImageData {
253            width: self.width,
254            height: self.height,
255            color_type: self.color_type,
256            depth: 8,
257            data,
258        })
259    }
260
261    fn import_image<'a>(
262        &'a self,
263        data: ImageData,
264        _filename: &str,
265        file: Box<dyn WriteSeek + 'a>,
266    ) -> Result<()> {
267        create_image(
268            data,
269            file,
270            self.opt_is_scrambled.unwrap_or(self.is_scrambled),
271        )
272    }
273}